package com.kaifamiao.jdbc.helper;

import java.io.IOException;
import java.io.InputStream;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;

public final class JdbcHelper {

    /* - - - - 定义常量(用于处理属性文件) - - - - */
    private static final String PREFIX = "jdbc.connection.";
    private static final String SUFFIX_DRIVER = ".driver";
    private static final String SUFFIX_URL = ".url";
    private static final String SUFFIX_USER = ".user";
    private static final String SUFFIX_PASSWORD = ".password";
    private static final String CONNECT = "connect";
    private static final String DEFAULT_CONNECT = "mysql";
    private static final String DEFAULT_DRIVER = "com.mysql.cj.jdbc.Driver";
    private static final String DEFAULT_URL = "jdbc:mysql://localhost:3306?serverTimezone=Asia/Chongqing";
    private static final String DEFAULT_USER = "root";
    private static final String DEFAULT_PASSWORD = "";
    private static final Properties PROPS = new Properties();

    /* - - - - 用来存储数据库连接信息的实例变量 - - - - */
    private String driver;
    private String url;
    private String user;
    private String password;
    /* - - - - 用来缓存数据库连接的实例变量 - - - - */
    private Connection conn;

    private JdbcHelper(){
        super();
    }

    public static JdbcHelper getInstance(){
        // 默认读取类路径根目录下的jdbc.properties文件
        return getInstance( "/jdbc.properties" );
    }

    public static JdbcHelper getInstance( String pathname ){
        JdbcHelper h = new JdbcHelper();
        h.read( pathname ); // 读取配置文件
        h.init(); // 初始化数据库连接信息
        h.load(); // 加载并注册数据库驱动
        h.connect(); // 建立数据库连接
        return h;
    }

    // 读取配置文件
    private void read( String pathname ) {
        // 获得当前实例(this)对应的类的 Class实例
        Class<?> c = this.getClass();
        // 获得用于读取 类路径下 指定文件(filename) 的字节输入流
        InputStream in = c.getResourceAsStream( pathname );
        try {
            // 从字节输入流中加载 "属性名-属性值" 对 到 Properties集合中
            PROPS.load(in);
            // 关闭字节输入流
            in.close();
        } catch ( IOException e ) {
            System.out.println( "读取资源文件时发生错误: " + e.getMessage() );
        }
    }

    // 初始化数据库连接信息
    private void init(){
        String connect = PROPS.getProperty( CONNECT , DEFAULT_CONNECT);
        connect = connect.isBlank() ? DEFAULT_CONNECT : connect;
        driver = PROPS.getProperty( PREFIX + connect + SUFFIX_DRIVER , DEFAULT_DRIVER );
        url = PROPS.getProperty( PREFIX + connect + SUFFIX_URL , DEFAULT_URL );
        user = PROPS.getProperty( PREFIX + connect + SUFFIX_USER , DEFAULT_USER );
        password = PROPS.getProperty( PREFIX + connect + SUFFIX_PASSWORD , DEFAULT_PASSWORD );
    }

    // 加载并注册驱动
    private void load(){
        try {
            Class.forName( driver );
        } catch (ClassNotFoundException cause) {
            String message = String.format( "驱动类%s不存在", driver );
            throw new RuntimeException( message , cause );
        }
    }

    // 建立连接
    private void connect(){
        try {
            conn = DriverManager.getConnection( url, user, password );
        } catch (SQLException cause) {
            String message = String.format( "数据库连接失败" );
            throw new RuntimeException( message , cause );
        }
    }

    /**
     * 根据给定的SQL语句创建 PreparedStatement 对象，并指示是否获取由数据库生成的键
     * @param sql 可能带有参数占位符的SQL语句
     * @param generate 若需要获得由数据库生成的键则传入 true ，否则传入 false 。
     * @return
     */
    private PreparedStatement prepare(String sql , boolean generate){
        try {
            return conn.prepareStatement( sql , generate ? Statement.RETURN_GENERATED_KEYS : Statement.NO_GENERATED_KEYS);
        } catch (SQLException cause) {
            throw new RuntimeException( "创建Statement失败" , cause );
        }
    }

    // 为PreparedStatement设置参数占位符的值
    private void setParameters( PreparedStatement ps , Object... params ) {
        if( params != null && params.length > 0 ) {
            try {
                for (int i = 0; i < params.length; i++) {
                    ps.setObject(i + 1, params[i]);
                }
            } catch ( SQLException cause ) {
                throw new RuntimeException( "SQL占位符参数设置错误", cause );
            }
        }
    }

    /**
     * "执行"DML语句并返回受语句影响的记录数目
     * @param sql 可能带有`SQL参数占位符`的DML语句
     * @param params 若第一个参数中包含`SQL参数占位符`，则需要通过变长参数依次传入各个占位符的值
     * @return
     */
    public int execute( String sql, Object... params ){
        PreparedStatement ps = this.prepare(sql, false);
        setParameters( ps , params );
        try {
            return ps.executeUpdate();
        } catch (SQLException cause) {
            throw new RuntimeException( "执行SQL时发生错误" , cause );
        } finally {
            this.release( ps );
        }
    }

    // 执行插入操作并返回由数据库生成的键
    public <T> T insert( String sql, Object... params ){
        T id = null ;
        PreparedStatement ps = this.prepare(sql, true);
        setParameters( ps , params );
        try {
            int count = ps.executeUpdate();
            // 如果仅插入一行数据，则只需要返回单个键即可
            if( count == 1 ) {
                ResultSet rs = ps.getGeneratedKeys();
                if( rs.next() ) {
                    id = (T)rs.getObject( 1 );
                }
                this.release( rs );
            }
            return id;
        } catch (SQLException cause) {
            throw new RuntimeException( "执行SQL时发生错误" , cause );
        } finally {
            this.release( ps );
        }
    }

    // 修改事务提交模式
    private void autocommit( boolean auto ){
        try {
            conn.setAutoCommit( auto );
        } catch (SQLException cause) {
            throw new RuntimeException( "修改事务提交模式失败" , cause);
        }
    }

    // 回滚事务
    private void commit(){
        try {
            conn.commit();
        } catch (SQLException cause) {
            throw new RuntimeException( "事务提交失败" , cause);
        }
    }

    // 回滚事务
    private void rollback(){
        try {
            conn.rollback();
        } catch (SQLException cause) {
            throw new RuntimeException( "事务回滚失败" , cause);
        }
    }

    // 在同一个事务中执行多个DML操作
    public void execute( Runner runner ){
        // 关闭事务自动提交
        this.autocommit(false);
        try {
            // 如何执行多个DML语句
            runner.doInTransaction();
            // 手动提交事务
            this.commit();
        } catch (Exception cause) {
            // 手动回滚事务
            this.rollback();
            // 打印异常轨迹
            throw new RuntimeException( "执行失败" , cause);
        }
        // 启用事务自动提交
        this.autocommit(true);
    }

    /**
     * "执行"查询语句并以List集合形式返回查询结果
     * @param sql 被"执行"的查询语句 (可以携带参数占位符)
     * @param holder 用于将结果集中的单行数据转换为指定类型的对象
     * @param params 若被"执行"查询语句中含有参数占位符，则依次传入它们的值
     * @param <E> 类型参数E用于明确集合中所存放的对象的类型
     * @return
     */
    public <E> List<E> query(String sql, RowHolder<E> holder, Object... params){
        // 创建一个用于存放E类型实例的集合
        List<E> list = new ArrayList<>();
        PreparedStatement ps = this.prepare(sql, false);
        setParameters( ps , params );
        try {
            ResultSet rs = ps.executeQuery();
            // 逐行处理结果集
            while( rs.next() ) {
                // 调用 holder 的 mapRow 方法 将 结果集中当前行各个列的数据封装到一个E类型的对象中
                E e = holder.mapRow(rs);
                list.add( e );
            }
            this.release( rs );
        } catch (SQLException cause) {
            throw new RuntimeException("查询失败", cause);
        }
        this.release( ps );
        return list;
    }

    /**
     * "执行"查询语句返回单个查询结果
     * @param sql 被"执行"的查询语句 (可以携带参数占位符、至多返回一行数据)
     * @param holder 用于将结果集中的单行数据转换为指定类型的对象
     * @param params 若被"执行"查询语句中含有参数占位符，则依次传入它们的值
     * @param <E> 类型参数E用于明确当前方法所返回对象的类型
     * @return
     */
    public <E> E single( String sql, RowHolder<E> holder, Object... params ){
        E e = null;
        PreparedStatement ps = this.prepare(sql, false);
        setParameters( ps , params );
        try {
            ResultSet rs = ps.executeQuery();
            // 处理结果集
            if( rs.next() ) {
                // 调用 holder 的 mapRow 方法 将 结果集中当前行各个列的数据封装到一个E类型的对象中
                e = holder.mapRow(rs);
            }
            this.release( rs );
        } catch (SQLException cause) {
            throw new RuntimeException("查询失败", cause);
        }
        this.release( ps );
        return e ;
    }

    // 释放资源
    private void release( AutoCloseable ac ) {
        if( ac != null ) {
            try {
                ac.close();
            } catch (Exception cause) {
                String message = String.format( "释放资源时发生错误" );
                throw new RuntimeException( message , cause );
            }
        }
    }

    // 释放资源
    public void release() {
        this.release( conn );
    }
}
